/**
 * Copyright (c) 2007-2008 Nearchos Paspallis
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * AspectSense
 *
 * Middleware for annotation-based, context-aware and adaptive services for
 * mobile and ubiquitous computing
 *
 * This project is an open-source effort, initiated by Nearchos Paspallis as
 * part of his PhD work at the University of Cyprus (http://www.ucy.ac.cy).
 *
 * More information about the project is available at "http://aspectsense.org".
 */

package org.aspectsense.metadata;

import org.aspectsense.annotations.*;
import org.aspectsense.framework.*;

import java.util.logging.Logger;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;

/**
 * <p/>
 * Date: Aug 7, 2007<br/>
 * Time: 3:37:06 PM
 * </p>
 *
 * @author Nearchos Paspallis [nearchos@cs.ucy.ac.cy]
 */
public class MetadataFactory
{
    private static final Logger logger = Logger.getLogger(MetadataFactory.class.getCanonicalName());

    /** Caching of {@link ComponentMetadata} is possible because the
     * {@link DefaultComponentMetadata} is immutable.
     */
    private static final Map<Class, ComponentMetadata> componentMetadataCache
            = new HashMap<Class, ComponentMetadata>();

    /** Caching of {@link ServiceMetadata} is possible because the
     * {@link DefaultServiceMetadata} is immutable.
     */
    private static final Map<Class, ServiceMetadata> serviceMetadataCache
            = new HashMap<Class, ServiceMetadata>();

    private static final Map<Class, ServiceUtilityMetadata> serviceUtilityMetadataCache
            = new HashMap<Class, ServiceUtilityMetadata>();

    private static final Map<Class, Class> cachedProvidedServices
            = new HashMap<Class, Class>();

    public static Class getCachedProvidedService(final Class componentType)
    {
        return cachedProvidedServices.get(componentType);
    }

    public static ComponentMetadata getComponentMetadata(final Class componentType)
    {
        if(componentType == null) throw new NullPointerException("Illegal null argument");

        if(componentMetadataCache.containsKey(componentType))
        {
            return componentMetadataCache.get(componentType);
        }

        org.aspectsense.annotations.Component componentAnnotation = null;

        final Set<org.aspectsense.annotations.Service> serviceAnnotations
                = new HashSet<org.aspectsense.annotations.Service>();

        final Annotation[] annotations = componentType.getDeclaredAnnotations();

        for(final Annotation annotation : annotations)
        {
            if(annotation instanceof org.aspectsense.annotations.Component)
            {
                if(componentAnnotation == null)
                {
                    componentAnnotation = (org.aspectsense.annotations.Component) annotation;
                }
                else
                {
                    logger.severe("Multiple annotations of type " + org.aspectsense.framework.Component.class);
                }
            }
            else if(annotation instanceof org.aspectsense.annotations.Service)
            {
                serviceAnnotations.add((org.aspectsense.annotations.Service) annotation);
            }
        }

        // check if instance of component
        final boolean isComponent = isComponent(componentType);
        if(!isComponent)
        {
            throw new RuntimeException("Invalid component type: " + componentType
                    + ": does not implement the aspectsense.framework.Component interface!");
        }
        if(componentAnnotation == null)
        {
            throw new RuntimeException("Invalid component type: " + componentType
                    + ": not annotated with required component annotation!");
        }

        final String displayID = componentAnnotation.displayID();
        final String description = componentAnnotation.description();

        // check if stateful
        final boolean isStateful = Stateful.class.isAssignableFrom(componentType);

        // each component must provide *exactly one* service
        if(serviceAnnotations.size() != 1)
        {
            throw new IllegalArgumentException("The specified component does not provide exactly one service!");
        }

        ServiceMetadata providedService = null;
        for(final org.aspectsense.annotations.Service serviceAnnotation : serviceAnnotations)
        {
            final Class serviceType = serviceAnnotation.serviceType();
            providedService = MetadataFactory.getServiceMetadata(serviceAnnotation, serviceType);

            cachedProvidedServices.put(componentType, serviceType);
        }

        assert providedService != null;

        final Map<String, RequiredServiceMetadata> requiredServices
                = new HashMap<String, RequiredServiceMetadata>();

        // populate set
        final Field [] fields = componentType.getDeclaredFields();
        for(final Field field : fields)
        {
            final RequiredService requiredService = field.getAnnotation(RequiredService.class);
            if(requiredService != null)
            {
                final Class serviceType = field.getType();
                if(org.aspectsense.framework.Service.class.isAssignableFrom(serviceType))
                {
                    final String serviceID = serviceType.getCanonicalName();
                    requiredServices.put(serviceID, MetadataFactory.getRequiredServiceMetadata(requiredService, serviceType));
                }
                else
                {
                    final String errorMessage = "Error! Annotated variation point: "
                            + requiredService.variationPointID()
                            + " is of a type different from " + org.aspectsense.framework.Service.class;

                    logger.severe(errorMessage);
                    throw new RuntimeException(errorMessage);
                }
            }
        }

        final ComponentMetadata componentMetadata = new DefaultComponentMetadata(
                displayID,
                componentType.getCanonicalName(),
                description,
                isStateful,
                providedService,
                requiredServices);

        componentMetadataCache.put(componentType, componentMetadata);

        return componentMetadata;
    }

    public static ServiceMetadata getServiceMetadata(
            final org.aspectsense.annotations.Service service,
            final Class serviceType)
    {
        if(service == null)
        {
            throw new NullPointerException("Invalid null argument: " + org.aspectsense.framework.Service.class);
        }

        if(serviceType == null)
        {
            throw new NullPointerException("Invalid null argument: " + Class.class);
        }

        final String serviceTypeCanonicalName = serviceType.getCanonicalName();

        // check if the actual Service is the same as the annotated one
        if(!service.serviceType().equals(serviceType))
        {
            throw new RuntimeException("The specified service type: " + serviceType
                    + ": does not match the annotated one: " + service.serviceType());
        }

        if(serviceMetadataCache.containsKey(serviceType))
        {
            return serviceMetadataCache.get(serviceType);
        }

        final String description = service.description();
        final boolean canBeDistributed = service.canBeDistributed();

        // check if instance of service
        final boolean isService = isService(serviceType);
        if(!isService)
        {
            throw new RuntimeException("Invalid service type: " + serviceType
                    + ": does not implement the aspectsense.framework.Service interface!");
        }

        final boolean isEndUserService = isEndUserService(serviceType);

        final ServiceMetadata serviceMetadata = new DefaultServiceMetadata(
                serviceTypeCanonicalName,
                description,
                isEndUserService,
                canBeDistributed
        );

        serviceMetadataCache.put(serviceType, serviceMetadata);

        return serviceMetadata;
    }

    public static RequiredServiceMetadata getRequiredServiceMetadata(
            final RequiredService requiredService,
            final Class serviceType)
    {
        // no caching needed for RequiredService (variation points)

        if(requiredService == null)
        {
            throw new NullPointerException("Invalid null argument: " + RequiredService.class);
        }

        if(serviceType == null)
        {
            throw new NullPointerException("Invalid null argument: " + Class.class);
        }

        final String requiredServiceCanonicalName = serviceType.getCanonicalName();
        final String variationPointID = requiredService.variationPointID();
        final String description = requiredService.description();
        final boolean mustBeLocal = requiredService.mustBeLocal();

        return new DefaultRequiredServiceMetadata(
                requiredServiceCanonicalName,
                variationPointID,
                description,
                mustBeLocal);
    }

    private static final Map<Class, Class> cachedEvaluatorTypes
            = new HashMap<Class, Class>();

    public static Class getCachedEvaluatorType(final Class componentType)
    {
        return cachedEvaluatorTypes.get(componentType);
    }

    public static ServiceUtilityMetadata getServiceUtilityMetadata(final Class componentType)
    {
        if(componentType == null)
        {
            throw new NullPointerException("Illegal null argument");
        }

        if(serviceUtilityMetadataCache.containsKey(componentType))
        {
            return serviceUtilityMetadataCache.get(componentType);
        }

        // Get the 'constants' and 'parameters' metadata from the class
        final Annotation [] allAnnotations
                = componentType.getDeclaredAnnotations();

        ServiceUtility serviceUtility = null;

        for(final Annotation annotation : allAnnotations)
        {
            if(annotation instanceof ServiceUtility)
            {
                if(serviceUtility == null)
                {
                    serviceUtility = (ServiceUtility) annotation;
                }
                else
                {
                    throw new IllegalArgumentException("The specified"
                            + " component does not provide exactly one "
                            + "ServiceUtility annotation!");
                }
            }
        }

        if(serviceUtility == null)
        {
            throw new IllegalArgumentException("The specified component does"
                    + "not specify the ServiceUtility metadata: "
                    + componentType);
        }

        final Class evaluatorType = serviceUtility.evaluatorType();
        cachedEvaluatorTypes.put(componentType, evaluatorType);

        final String targetServiceTypeCanonicalName = serviceUtility.targetServiceType().getCanonicalName();
        final String description = serviceUtility.description();
        final String evaluatorTypeCanonicalName = evaluatorType.getCanonicalName();
        final String [] constants = serviceUtility.constants();
        final String utilityFunctionS = serviceUtility.utilityFunction();

        final ServiceUtilityMetadata serviceUtilityMetadata = new DefaultServiceUtilityMetadata(
                targetServiceTypeCanonicalName,
                description,
                evaluatorTypeCanonicalName,
                constants,
                utilityFunctionS);

        serviceUtilityMetadataCache.put(componentType, serviceUtilityMetadata);

        return serviceUtilityMetadata;
    }

    public static String getComponentTypeCanonicalName(final Class componentType)
    {
        if(componentType == null)
        {
            throw new NullPointerException("Invalid null argument");
        }

        return componentType.getCanonicalName();
    }

    public static String getServiceTypeCanonicalName(final Class serviceType)
    {
        if(serviceType == null)
        {
            throw new NullPointerException("Invalid null argument");
        }

        return serviceType.getCanonicalName();
    }

    public static boolean isComponent(final Class componentType)
    {
        return org.aspectsense.framework.Component.class.isAssignableFrom(componentType);
    }

    public static boolean isService(final Class serviceType)
    {
        return org.aspectsense.framework.Service.class.isAssignableFrom(serviceType);
    }

    public static boolean isEndUserService(final Class serviceType)
    {
        return EndUserService.class.isAssignableFrom(serviceType);
    }
}